---
id: pluginsmanager
title: 插件系统
---

### 定义

命名空间：TouchSocket.Core <br/>
程序集：[TouchSocket.Core.dll](https://www.nuget.org/packages/TouchSocket.Core)


## 一、说明
插件系统是一组能实现多播订阅的，可中断的触发器，其主要功能就是实现类似事件、委托的通知消息。其设计核心来自于Aspnetcore的中间件，它有着和中间件一样的使用体验，同时也有着更高的灵活性和自由度。

## 二、产品特点

- 简单易用。
- 易扩展。

## 三、产品应用场景

- 所有可以使用事件。委托的场景。

## 四、与事件、委托相比

#### 4.1 优点

1. 订阅的时候可以不用知道被订阅方是谁，只需要知道要订阅什么通知即可。
2. 订阅可以随时中断。例如：当事件多播的时候，即使其中一个订阅方已经处理，触发也不会停止，这样会造成资源浪费。
3. 订阅回调。例如：第一个订阅方，想知道本次触发的最终结果是否已被处理时，委托则做不到。而插件则可以。
4. 插件可以被继承，可以被扩展。
5. 插件可以被注入。
6. 插件可以独立负责相关功能，实现功能独立。可模块化功能。

#### 4.2 缺点

1. 使用插件会损耗一部分性能。

## 五、使用

#### 5.1 声明插件接口及事件类

```csharp showLineNumbers
public class MyPluginEventArgs : PluginEventArgs
{
    public string Words { get; set; }
}

/// <summary>
/// 定义一个插件接口，使其继承<see cref="IPlugin"/>
/// </summary>
public interface ISayPlugin : IPlugin
{
    /// <summary>
    /// Say。定义一个插件方法，必须遵循：
    /// 1.必须是两个参数，第一个参数可以是任意类型，一般表示触发源。第二个参数必须继承<see cref="PluginEventArgs"/>
    /// 2.返回值必须是Task。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// <returns></returns>
    Task Say(object sender, MyPluginEventArgs e);
}
```

:::caution 注意事项

确定插件唯一的是插件中的`方法名`，所以该方法名应该尽量能确定唯一性。

:::  

:::tip 提示

一个插件接口中，可以声明多个插件方法。但是一般建议只声明一个，这样使用方在实现接口时更加灵活，避免实现多余的接口。

:::  

#### 5.2 实现插件接口

```csharp showLineNumbers 
 public class SayHelloPlugin : PluginBase, ISayPlugin
 {
     public async Task Say(object sender, MyPluginEventArgs e)
     {
         Console.WriteLine($"{this.GetType().Name}------Enter");
         if (e.Words == "hello")
         {
             Console.WriteLine($"{this.GetType().Name}------Say");
             //当满足的时候输出，且不在调用下一个插件。

             //亦或者设置e.Handled = true，即使调用下一个插件，也会无效
             
             return;
         }
         await e.InvokeNext();
         Console.WriteLine($"{this.GetType().Name}------Leave");
     }
 }

 public class SayHiPlugin : PluginBase, ISayPlugin
 {
     public async Task Say(object sender, MyPluginEventArgs e)
     {
         Console.WriteLine($"{this.GetType().Name}------Enter");
         if (e.Words == "hi")
         {
             Console.WriteLine($"{this.GetType().Name}------Say");
             //当满足的时候输出，且不在调用下一个插件。

             //亦或者设置e.Handled = true，即使调用下一个插件，也会无效
             
             return;
         }

         await e.InvokeNext();
         Console.WriteLine($"{this.GetType().Name}------Leave");
     }
 }
 public class LastSayPlugin : PluginBase, ISayPlugin
 {
     public async Task Say(object sender, MyPluginEventArgs e)
     {
         Console.WriteLine($"{this.GetType().Name}------Enter");
         Console.WriteLine($"您输入的{e.Words}似乎不被任何插件处理");
         await e.InvokeNext();
         Console.WriteLine($"{this.GetType().Name}------Leave");
     }
 }
```

:::tip 提示

实现的插件，可以继承`PluginBase`，然后实现所需的插件接口，这样能简化实现过程。但是如果该类型已经拥有基类，则直接实现所需插件接口的全部内容即可。

:::  

:::caution 注意事项

插件实现的方法，一般建议是常规实现，如果是显式接口实现，则**可能**在net framework架构上不可用。

:::  

#### 5.3 触发插件接口

```csharp {11}
IPluginsManager pluginsManager = new PluginsManager(new Container())
{
    Enable = true//必须启用
};

pluginsManager.Add<SayHelloPlugin>();
pluginsManager.Add<SayHiPlugin>();
pluginsManager.Add<LastSayPlugin>();

//订阅插件，不仅可以使用声明插件的方式，还可以使用委托。
pluginsManager.Add(nameof(ISayPlugin.Say), () => 
{
    Console.WriteLine("在Action1中获得");
});

pluginsManager.Add(nameof(ISayPlugin.Say), async (MyPluginEventArgs e) =>
{
    Console.WriteLine("在Action2中获得");
    await e.InvokeNext();
});

while (true)
{
    Console.WriteLine("请输入hello，或者hi");
    pluginsManager.Raise(nameof(ISayPlugin.Say), new object(), new MyPluginEventArgs()
    {
        Words = Console.ReadLine()
    });
}
```

:::tip 提示

使用委托订阅插件时，如果订阅时，不包含`PluginEventArgs`，即不用显式`e.InvokeNext`。如果包含，则需要显式`e.InvokeNext`。

:::  

#### 5.4 执行结果

按照上述代码代码逻辑，我们声明了一个名为`ISayPlugin`的插件接口，里面包含一个`Say`的方法。然后分别创建了`SayHelloPlugin`、`SayHiPlugin`、`LastSayPlugin`三个类去实现`ISayPlugin`接口。然后将该三个类都添加至插件管理器中，然后触发`Say`方法。同时传入不同的参数。

当Words=test时，`SayHelloPlugin`和`SayHiPlugin`均不满足处理条件，所以会将数据转至下一个插件，直到`LastSayPlugin`插件。然后当`LastSayPlugin`处理结束以后，处理结果又按照`LastSayPlugin`、`SayHiPlugin`、`SayHelloPlugin`的顺序退出插件。这样，即使`SayHelloPlugin`无法处理该数据，也能得知该数据最终有没有被处理。

当Words=hello时，`SayHelloPlugin`满足处理条件，并且终止插件的继续传递。

```
请输入hello，或者hi
test
SayHelloPlugin------Enter
SayHiPlugin------Enter
LastSayPlugin------Enter
您输入的test似乎不被任何插件处理
LastSayPlugin------Leave
SayHiPlugin------Leave
SayHelloPlugin------Leave

请输入hello，或者hi
hello
SayHelloPlugin------Enter
SayHelloPlugin------Say

请输入hello，或者hi
hi
SayHelloPlugin------Enter
SayHiPlugin------Enter
SayHiPlugin------Say
SayHelloPlugin------Leave
请输入hello，或者hi
```

## 六、插件特性

【执行顺序】
按照插件的添加顺序依次执行。

【中断传递】
当某个插件在响应时，如果设置e.Handled=true，或者没有调用下一个插件，则该数据将**不会**再触发后续的插件。


## 七、提升插件性能

### 7.1 插件性能测试

如下图所示，添加10个插件，并且调用10000次。即：单个插件方法被调用10w次。

分别在net6.0，netcore3.1，net4.6.1上进行测试。测试项依次为：

1. DirectRun（直接调用）
2. ActionRun（委托调用）
3. MethodInfoRun（反射调用）
4. ExpressionRun（表达式树调用）
5. PluginRun（插件调用）
6. PluginActionRun（插件委托调用）

实际上插件内部使用的是IL调用，所以即使调用上有迭代，性能上也和直接调用的表达式树差不多（这里插件是采用递归的方式迭代，而表达式树测试则是直接for迭代）。但是总体而言性能上是有缺失的。

但是当使用插件委托调用时，性能上会有所提升。和直接调用相比，虽然性能降低了20%。但是这已经是委托调用的极限，且没有任何动态代码的生成。这意味着在unity调用也是完全可行的。

<img src={require('@site/static/img/docs/pluginsmanager-1.png').default} />

### 7.2 注册委托

注册委托，使用委托，可以提升插件性能。

```csharp showLineNumbers
//订阅插件，不仅可以使用声明插件的方式，还可以使用委托。
pluginsManager.Add(nameof(ISayPlugin.Say), () => 
{
    Console.WriteLine("在Action1中获得");
});
```

其次，不仅可以直接使用委托，还可以在插件里面注册方法为委托。

```csharp showLineNumbers 
public class SayHelloAction: PluginBase
{
    protected override void Loaded(IPluginsManager pluginsManager)
    {
        base.Loaded(pluginsManager);

        //注册本地方法为委托
        pluginsManager.Add<object,MyPluginEventArgs>(nameof(ISayPlugin.Say),this.Say);
    }

    public async Task Say(object sender, MyPluginEventArgs e)
    {
        Console.WriteLine($"{this.GetType().Name}------Enter");
        if (e.Words == "helloaction")
        {
            Console.WriteLine($"{this.GetType().Name}------Say");
            //当满足的时候输出，且不在调用下一个插件。

            //亦或者设置e.Handled = true，即使调用下一个插件，也会无效
            
            return;
        }
        await e.InvokeNext();
        Console.WriteLine($"{this.GetType().Name}------Leave");
    }
}
```

:::caution 注意事项

注册方法为委托时，不要再实现接口，避免重复调用。

:::  

### 7.3 源生成插件委托

使用源生成器，直接可以生成委托调用，但是这要求你的编译器支持，一般（vs2022以上、或者Rider等）。

然后使用`GeneratorPlugin`特性标记所需方法即可。

```csharp showLineNumbers
public partial class SayHelloGenerator : PluginBase
{
    //如果在代码里，继承了PluginBase，并且没有显示重写Loaded
    //则在源生成时，会自己生成重写代码。
    //如果显示重写了Loaded。就需要自己手动调用RegisterPlugins。不然插件不会生效的。
    //protected override void Loaded(IPluginsManager pluginsManager)
    //{
    //    base.Loaded(pluginsManager);
    //    this.RegisterPlugins(pluginsManager);
    //}

    /// <summary>
    /// 使用源生成插件
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// <returns></returns>
    [GeneratorPlugin(nameof(ISayPlugin.Say))]
    public async Task Say(object sender, MyPluginEventArgs e)
    {
        Console.WriteLine($"{this.GetType().Name}------Enter");
        if (e.Words == "hellogenerator")
        {
            Console.WriteLine($"{this.GetType().Name}------Say");
            //当满足的时候输出，且不在调用下一个插件。

            //亦或者设置e.Handled = true，即使调用下一个插件，也会无效
            
            return;
        }
        await e.InvokeNext();
        Console.WriteLine($"{this.GetType().Name}------Leave");
    }
}
```

:::caution 注意事项

1. 使用源生成器时，插件类必须声明为部分类（`partial`），且不能是类中类。
2. 使用源生成器时，不要再实现接口，避免重复调用。
3. 使用源生成器时，如果在代码里，继承了`PluginBase`，并且没有显式重写`Loaded`，那生成的代码会自动重写`Loaded`。如果显式重写了`Loaded`，就需要自己手动调用`RegisterPlugins`。不然插件不会生效的。

:::  

:::tip 提示

使用源生成器时，可以先使用插件接口的方式，实现所需的方法。然后再删除接口，添加`GeneratorPlugin`特性，可以简化很多书写。

:::  

:::tip 建议

如果您在性能与易用性之间做权衡的话，我们建议可以使用插件委托实现（或者源生成器）。

:::  

[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/Core/PluginConsoleApp)